/*
 * User.java
 *
 * Created on July 1, 2007, 1:25 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.auth.service.db;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import org.infoset.xml.Element;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.XMLException;
import org.milowski.db.DB;
import org.milowski.db.DBConnection;
import org.milowski.db.DBIterator;
import org.milowski.db.DBObject;
import org.milowski.db.DBQueryHandler;
import org.milowski.db.DBResultConstructor;
import org.milowski.db.DBUpdateHandler;
import org.milowski.db.Slot;

/**
 *
 * @author alex
 */
public class User extends DBObject<AuthDB> implements XMLObject
{
   
   static public class Authenticated {
      AuthDB db;
      int dbid;
      User user;
      Date expiration;
      Date created;
      UUID session;
      Realm realm;
      
      public Authenticated(AuthDB db,int id,UUID session,Date expiration,Date created,User user,Realm realm) {
         this.db = db;
         this.dbid = id;
         this.session = session;
         this.expiration = expiration;
         this.created = created;
         this.user = user;
         this.realm = realm;
      }
      
      public User getUser() {
         return user;
      }
      
      public Realm getRealm() {
         return realm;
      }
      
      public UUID getSession() {
         return session;
      }
      public Date getExpiration() {
         return expiration;
      }
      public Date getCreated() {
         return created;
      }
      
      public boolean isExpired() {
         return expiration!=null && expiration.getTime()<System.currentTimeMillis();
      }
      
      public void delete()
         throws SQLException
      {
         DBConnection connection = db.getConnection();
         try {
            connection.deleteById(AuthDB.DELETE_AUTHENTICATED, dbid);
         } finally {
            db.release(connection);
         }
      }
   }
   
   String alias;
   UUID uuid;
   String name;
   String email;
   
   /** Creates a new instance of User */
   public User(AuthDB db,int id,UUID uuid,String alias,String name,String email)
   {
      super(db,id);
      this.alias = alias;
      this.uuid = uuid;
      this.name = name;
      this.email = email;
   }
   
   public void delete() 
      throws SQLException
   {
      DBConnection connection = db.getConnection();
      try {
         connection.deleteById(AuthDB.DELETE_USER_ROLES_BY_USER, id);
         connection.deleteById(AuthDB.DELETE_AUTHENTICATION_BY_USER, id);
         connection.deleteById(AuthDB.DELETE_USER_ALIAS_BY_USER, id);
         connection.query(AuthDB.REALM_USERS_BY_USER, new DBQueryHandler() {
            public void prepare(PreparedStatement s) 
               throws SQLException
            {
               s.setInt(1, id);
            }
            public void onResults(ResultSet set)
               throws SQLException
            {
               while (set.next()) {
                  Realm realm = db.realmCache.get(set.getInt(2));
                  RealmUser user = db.realmUserCaches.get(realm).get(set.getInt(1));
                  user.delete();
                  db.realmUserCaches.get(realm).remove(user.getId());
               }
            }
         });
         connection.deleteById(AuthDB.DELETE_USER, id);
         db.userCache.remove(id);
      } finally {
         db.release(connection);
      }
   }
   
   public void setPassword(String password)
      throws SQLException,NoSuchAlgorithmException
   {
      String md5 = md5Password(password);
      setEncryptedPassword("md5",md5);
   }
   
   public void setEncryptedPassword(String algorithm,final String value)
      throws SQLException,NoSuchAlgorithmException
   {
      if (!algorithm.equals("md5")) {
         throw new NoSuchAlgorithmException("Algorithm "+algorithm+" is not supported.");
      }
      DBConnection connection = db.getConnection();
      try {
         connection.deleteById(AuthDB.DELETE_AUTHENTICATION_BY_USER, id);
         connection.create(AuthDB.CREATE_AUTHENTICATION, -1,new DBUpdateHandler() {
            public void prepare(PreparedStatement s) 
               throws SQLException
            {
               s.setInt(1,id);
               s.setString(2,value);
            }
         });
      } finally {
         db.release(connection);
      }
   }
   
   public void addRole(final Role role)
      throws SQLException
   {
      DBConnection connection = db.getConnection();
      try {
         connection.update(AuthDB.DELETE_USER_ROLE, new DBUpdateHandler() {
            public void prepare(PreparedStatement s) 
               throws SQLException
            {
               s.setInt(1,id);
               s.setInt(2,role.getId());
            }
         });
         connection.update(AuthDB.ADD_USER_ROLE, new DBUpdateHandler() {
            public void prepare(PreparedStatement s) 
               throws SQLException
            {
               s.setInt(1,id);
               s.setInt(2,role.getId());
            }
         });
      } finally {
         db.release(connection);
      }
   }
   
   public boolean hasRole(final Role role)
      throws SQLException
   {
      
      final Slot<Boolean> hasRole = new Slot<Boolean>(false);
      DBConnection connection = db.getConnection();
      try {
         connection.query(AuthDB.USER_HAS_ROLE, new DBQueryHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
               s.setInt(2,role.getId());
            }
            public void onResults(ResultSet set)
               throws SQLException
            {
               hasRole.set(set.next());
            }
         });
      } finally {
         db.release(connection);
      }
      return hasRole.get();
   }
   
   public boolean hasPermission(Permission permission)
      throws SQLException 
   {
      boolean found = false;
      Iterator<Role> roles = getRoles();
      while (roles.hasNext()) {
         if (roles.next().hasPermission(permission)) {
            found = true;
         }
      }
      return found;
   }
   
   public boolean removeRole(final Role role)
      throws SQLException
   {
      DBConnection connection = db.getConnection();
      try {
         return connection.update(AuthDB.USER_HAS_ROLE, new DBUpdateHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
               s.setInt(2,role.getId());
            }
         })>0;
      } finally {
         db.release(connection);
      }
   }

   public Iterator<Role> getRoles() 
      throws SQLException
   {
      final Slot<Iterator<Role>> result = new Slot<Iterator<Role>>();
      final DBConnection connection = db.getConnection();
      try {
         connection.query(AuthDB.USER_ROLES, new DBQueryHandler() {
         public boolean shouldClose() { return false; }
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
            }
            public void onResults(ResultSet set) 
            {
               result.set(new DBIterator<Role>(set,new DBResultConstructor<Role>() {
                  public Role newInstance(ResultSet set) 
                     throws SQLException
                  {
                     return db.roleCache.get(set.getInt(1));
                  }
               },db,connection));
            }
         });
      } catch (SQLException ex) {
         db.release(connection);
         throw ex;
      }
      return result.get();
   }
   
   public String getAlias()
   {
      return alias;
   }

   
   public boolean changeAlias(final String alias)
      throws SQLException
   {
      if (alias==null && this.alias!=null) {
         DBConnection connection = db.getConnection();
         try {
            connection.update(AuthDB.DELETE_USER_ALIAS, new DBUpdateHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setInt(1,id);
               }
            });
         } finally {
            db.release(connection);
         }
         this.alias = null;
         db.userCache.removeByName(this.alias);
         return true;
      }
      if (alias.equals(this.alias)) {
         return true;
      }
      if (!db.isUserAliasAvailable(alias)) {
         return false;
      }
      if (this.alias!=null) {
         db.userCache.removeByName(this.alias);
         DBConnection connection = db.getConnection();
         try {
            connection.update(AuthDB.CHANGE_USER_ALIAS, new DBUpdateHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setString(1,alias);
                  s.setString(2,id+"-"+alias);
                  s.setInt(3,id);
               }
            });
         } finally {
            db.release(connection);
         }
      } else {
         DBConnection connection = db.getConnection();
         try {
            connection.update(AuthDB.CREATE_USER_ALIAS, new DBUpdateHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setInt(1,id);
                  s.setString(2,alias);
                  s.setString(3,id+"-"+alias);
               }
            });
         } finally {
            db.release(connection);
         }
      }
      this.alias = alias;
      return true;
   }
   
   public UUID getUUID()
   {
      return uuid;
   }

   public String getName()
   {
      return name;
   }

   public void setName(final String name)
      throws SQLException
   {
      DBConnection connection = db.getConnection();
      try {
         connection.update(AuthDB.CHANGE_USER_NAME, new DBUpdateHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               if (name==null) {
                  s.setNull(1,Types.VARCHAR);
               } else {
                  s.setString(1,name);
               }
               s.setInt(2,id);
            }
         });
      } finally {
         db.release(connection);
      }
      this.name = name;
   }
   
   public String getEmail()
   {
      return email;
   }
   
   public void setEmail(final String email)
      throws SQLException
   {
      DBConnection connection = db.getConnection();
      try {
         connection.update(AuthDB.CHANGE_USER_NAME, new DBUpdateHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               if (email==null) {
                  s.setNull(1,Types.VARCHAR);
               } else {
                  s.setString(1,email);
               }
               s.setInt(2,id);
            }
         });
      } finally {
         db.release(connection);
      }
      this.email = email;
   }
   
   public boolean equals(Object obj) {
      return obj instanceof User && ((User)obj).getUUID().equals(uuid);
   }
   
   public Authenticated isAuthenticated(UUID session)
      throws SQLException
   {
      return isAuthenticated(null,session);
   }
   
   public Authenticated isAuthenticated(final Realm realm,final UUID session)
      throws SQLException
   {
      final Slot<Authenticated> retval = new Slot<Authenticated>();
      if (realm==null) {
         DBConnection connection = db.getConnection();
         try {
            connection.query(AuthDB.USER_AUTHENTICATED, new DBQueryHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setInt(1,id);
                  s.setString(2,session.toString());
               }
               public void onResults(ResultSet set) 
                  throws SQLException
               {
                  if (set.next()) {
                     Timestamp created = set.getTimestamp(2);
                     Timestamp expiration = set.getTimestamp(3);
                     Authenticated auth = new Authenticated(db,set.getInt(1),session,created,expiration,User.this,realm);
                     retval.set(auth);
                  }
               }
            });
            if (retval.get().isExpired()) {
               // the session has expired
               retval.get().delete();
               retval.set(null);
            }
            
         } finally {
            db.release(connection);
         }
      } else {
         DBConnection connection = db.getConnection();
         try {
            connection.query(AuthDB.REALM_USER_AUTHENTICATED, new DBQueryHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setInt(1,id);
                  s.setInt(2,realm.getId());
                  s.setString(3,session.toString());
               }
               public void onResults(ResultSet set) 
                  throws SQLException
               {
                  if (set.next()) {
                     Timestamp created = set.getTimestamp(2);
                     Timestamp expiration = set.getTimestamp(3);
                     Authenticated auth = new Authenticated(db,set.getInt(1),session,created,expiration,User.this,realm);
                     retval.set(auth);
                  }
               }
            });
            if (retval.get().isExpired()) {
               // the session has expired
               retval.get().delete();
               retval.set(null);
            }
            
         } finally {
            db.release(connection);
         }
      }
      return retval.get();
   }
   
   public Authenticated authenticate(String password,long expires)
      throws SQLException,NoSuchAlgorithmException
   {
      return authenticate(null,null,password,expires);
   }
   
   public Authenticated authenticate(Realm realm,String password,long expires)
      throws SQLException,NoSuchAlgorithmException
   {
      return authenticate(realm,null,password,expires);
   }
   
   public Authenticated authenticate(final Realm realm,final UUID session,String password,long expires)
      throws SQLException,NoSuchAlgorithmException
   {
      if (session!=null) {
         // delete any session authentication information first
         if (realm==null) {
            DBConnection connection = db.getConnection();
            try {
               connection.update(AuthDB.DELETE_USER_AUTHENTICATION, new DBUpdateHandler() {
                  public void prepare(PreparedStatement s)
                     throws SQLException
                  {
                     s.setInt(1,id);
                     s.setString(2,session.toString());
                  }
               });
            } finally {
               db.release(connection);
            }
         } else {
            DBConnection connection = db.getConnection();
            try {
               connection.update(AuthDB.DELETE_REALM_USER_AUTHENTICATION, new DBUpdateHandler() {
                  public void prepare(PreparedStatement s)
                     throws SQLException
                  {
                     s.setInt(1,id);
                     s.setInt(2,realm.getId());
                     s.setString(3,session.toString());
                  }
               });
            } finally {
               db.release(connection);
            }
         }
      }

      // delete any expired sessions for the user
      final Timestamp created = new Timestamp(System.currentTimeMillis());
      DBConnection connection = db.getConnection();
      try {
         connection.update(AuthDB.DELETE_EXPIRED_SESSIONS, new DBUpdateHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
               s.setTimestamp(2,created);
            }
         });
      } finally {
         db.release(connection);
      }
      
      // check the internal password
      if (checkPassword(password)) {
         // We have the correct password
         int dbid = -1;
         if (expires>0) {
            //construct an auth record with a session id
            final UUID theSession = session==null ? UUID.randomUUID() : session;
            final Timestamp expiration = new Timestamp(created.getTime()+expires);
            connection = db.getConnection();
            try {
               connection.update(AuthDB.CREATE_AUTHENTICATED, new DBUpdateHandler() {
                  public void prepare(PreparedStatement s)
                     throws SQLException
                  {
                     s.setInt(1,id);
                     if (realm==null) {
                        s.setNull(2,Types.INTEGER);
                     } else {
                        s.setInt(2,realm.getId());
                     }
                     s.setString(3,theSession.toString());
                     s.setTimestamp(4,created);
                     s.setTimestamp(5,expiration);
                  }
               });
            } finally {
               db.release(connection);
            }
            return new Authenticated(db,dbid,theSession,created,expiration,this,realm);
         } else {
            // the auth expires now
            return new Authenticated(db,dbid,session,created,created,this,realm);
         }
      }
      return null;
   }
   
   public Authenticated selfAuthenticate(final Realm realm,long expires)
      throws SQLException,NoSuchAlgorithmException
   {

      // delete any expired sessions for the user
      final Timestamp created = new Timestamp(System.currentTimeMillis());
      DBConnection connection = db.getConnection();
      try {
         connection.update(AuthDB.DELETE_EXPIRED_SESSIONS, new DBUpdateHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
               s.setTimestamp(2,created);
            }
         });
      } finally {
         db.release(connection);
      }
      
      int dbid = -1;
      if (expires>0) {
         //construct an auth record with a session id
         final UUID session = UUID.randomUUID();
         final Timestamp expiration = new Timestamp(created.getTime()+expires);
         connection = db.getConnection();
         try {
            connection.update(AuthDB.CREATE_AUTHENTICATED, new DBUpdateHandler() {
               public void prepare(PreparedStatement s)
                  throws SQLException
               {
                  s.setInt(1,id);
                  if (realm==null) {
                     s.setNull(2,Types.INTEGER);
                  } else {
                     s.setInt(2,realm.getId());
                  }
                  s.setString(3,session.toString());
                  s.setTimestamp(4,created);
                  s.setTimestamp(5,expiration);
               }
            });
         } finally {
            db.release(connection);
         }
         return new Authenticated(db,dbid,session,created,expiration,this,realm);
      } else {
         return null;
      }
   }
   
   
   public Authenticated recover(Realm realm)
      throws SQLException,NoSuchAlgorithmException
   {
      // expires in one hour
      long expires = (60*60*1000);
      return selfAuthenticate(realm,expires);
   }
   
   public boolean checkPassword(String password)
      throws SQLException,NoSuchAlgorithmException
   {
      final Slot<Boolean> found = new Slot<Boolean>(false);
      final String encrypted = md5Password(password);
      DBConnection connection = db.getConnection();
      try {
         connection.query(AuthDB.CHECK_PASSWORD, new DBQueryHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
               s.setString(2,encrypted);
            }
            public void onResults(ResultSet set) 
               throws SQLException
            {
               found.set(set.next());
            }
         });
      } finally {
         db.release(connection);
      }
      return found.get();
   }
   
   public String getEncryptedPassword() 
      throws SQLException
   {
      final Slot<String> found = new Slot<String>();
      DBConnection connection = db.getConnection();
      try {
         connection.query(AuthDB.ENCRYPTED_PASSWORD, new DBQueryHandler() {
            public void prepare(PreparedStatement s)
               throws SQLException
            {
               s.setInt(1,id);
            }
            public void onResults(ResultSet set) 
               throws SQLException
            {
               if (set.next()) {
                  found.set(set.getString(1));
               }
            }
         });
      } finally {
         db.release(connection);
      }
      return found.get();
   }
   
   
   public void generate(ItemConstructor constructor,ItemDestination dest)
      throws XMLException
   {
      generate(constructor,dest,false,true);
   }

   public void generate(ItemConstructor constructor,ItemDestination dest,boolean contents)
      throws XMLException
   {
      generate(constructor,dest,false,contents);
   }

   public void generate(ItemConstructor constructor,ItemDestination dest,boolean showPassword,boolean showRoles)
      throws XMLException
   {
      Element user = constructor.createElement(XML.USER_NAME);
      user.setAttributeValue("id",uuid.toString());
      if (alias!=null) {
         user.setAttributeValue("alias",alias);
      }
      if (showPassword) {
         try {
            String md5 = getEncryptedPassword();
            if (md5!=null) {
               user.setAttributeValue("password-md5",md5);
            }
         } catch (SQLException ex) {
            throw new XMLException("Cannot get password from database for user "+uuid,ex);
         }
      }
      dest.send(user);
      if (name!=null) {
         dest.send(constructor.createElement(XML.NAME_NAME));
         dest.send(constructor.createCharacters(name));
         dest.send(constructor.createElementEnd(XML.NAME_NAME));
      }
      if (email!=null) {
         dest.send(constructor.createElement(XML.EMAIL_NAME));
         dest.send(constructor.createCharacters(email));
         dest.send(constructor.createElementEnd(XML.EMAIL_NAME));
      }
      if (showRoles) {
         try {
            dest.send(constructor.createElement(XML.ROLES_NAME));
            Iterator<Role> roles = getRoles();
            boolean first = true;
            while (roles.hasNext()) {
               if (first) {
                  constructor.createCharacters("\n");
                  first = false;
               }
               roles.next().generate(constructor,dest,false);
               constructor.createCharacters("\n");
            }
            dest.send(constructor.createElementEnd(XML.ROLES_NAME));
         } catch (SQLException ex) {
            throw new XMLException("Cannot get user roles.",ex);
         }
      }
      dest.send(constructor.createElementEnd(XML.USER_NAME));
   }
   
   public static boolean isAlias(String alias)
   {
      int len = alias.length();
      for (int i=0; i<len; i++) {
         char ch = alias.charAt(i);
         if (!Character.isLetterOrDigit(ch) && ch!='.' && ch!='-') {
            return false;
         }
      }
      return true;
   }
   
   private static String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
   public static String md5Password(String password)
      throws java.security.NoSuchAlgorithmException
   {
      MessageDigest md5 = MessageDigest.getInstance( "MD5" );
      md5.update( password.getBytes() );
      byte[] b = md5.digest();
      StringBuilder buf = new StringBuilder( b.length * 2 );
      for ( int i = 0; i < b.length; i++ ) {
         int n = b[i];
         if ( n < 0 ) {
            n = 256 + n;
         }
         int d1 = n / 16;
         int d2 = n % 16;
         buf.append( hex[d1] );
         buf.append( hex[d2] );
      }
      return buf.toString();
   }
   
   public static void main(String [] args)
   {
      try {
         for (int i=0; i<args.length; i++) {
            System.out.println(md5Password(args[i]));
         }
      } catch(Exception ex) {
         ex.printStackTrace();
      }
   }
   
}
